﻿#region 참조 네임스페이스

using Skying.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;

#endregion

namespace Skying.Net.Sockets
{
	/// <summary>
	/// <see cref="BaseSocket"/>을 기반으로 하여 TCP 소켓 리스너를 구현한다.
	/// </summary>
	/// <remarks>
	/// <para><see cref="BaseSocket"/>을 상속하는 이유는 이벤트 인터페이스의 공유와 공용 상수와 필드의 사용을 위함이다.</para>
	/// <para><see cref="BaseSocket"/>을 상속하는 것 뿐만 아니라, <see cref="TcpClient"/>와의 통신을 위한 매개체로 <see cref="BaseSocket"/>을 사용한다.</para>
	/// </remarks>
	public sealed class TcpListener : BaseSocket
	{
		#region 변수
		
		/// <summary>
		/// 허용된 클라이언트 사전을 나타낸다.
		/// </summary>
		private Dictionary<Guid, BaseSocket> _acceptedClients;

		/// <summary>
		/// 데이터를 송신 가능한 클라이언트의 아이디 목록을 나타낸다.
		/// </summary>
		private IList<Guid> _sendableClients;

		/// <summary>
		/// 데이터를 수신 가능한 클라이언트의 아이디 목록을 나타낸다.
		/// </summary>
		private IList<Guid> _receivableOnlyClients;

		#endregion
		
		#region 프로퍼티

		/// <summary>
		/// 리스너가 수신 대기 상태인지 아닌지를 가져온다.
		/// </summary>
		public bool IsListening
		{
			get;
			private set;
		}

		/// <summary>
		/// 허용된 클라이언트의 개수를 가져온다.
		/// </summary>
		public int ClientsCount
		{
			get
			{
				return this._acceptedClients.Count;
			}
		}

		/// <summary>
		/// 클라이언트 목록을 가져온다.
		/// </summary>
		public IList<BaseSocket> Clients
		{
			get
			{
				IList<BaseSocket> clients = null;

				lock (this._acceptedClients)
				{
					clients = new List<BaseSocket>(this._acceptedClients.Values);
				}

				return clients;
			}
		}

		/// <summary>
		/// TCP 리스너가 클라이언트을 수신할 수 있는 상태가 됐을 때 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> ListeningStarted
		{
			get;
			set;
		}

		/// <summary>
		/// TCP 리스너가 클라이언트로부터 수신을 중지하고 포트 바인딩을 해제했을 때 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> ListeningStopped
		{
			get;
			set;
		}

		/// <summary>
		/// 클라이언트의 접속이 허용됐을 때 실행된다.
		/// </summary>
		public Action<AsyncResultEventArgs<BaseSocket>> AcceptCompleted
		{
			get;
			set;
		}

		/// <summary>
		/// 클라이언트의 접속이 끊어졌을 때 실행된다.
		/// </summary>
		public Action<AsyncResultEventArgs<BaseSocket>> ClientDisconnected
		{
			get;
			set;
		}

		#endregion

		#region 생성자/소멸자

		public TcpListener(int port, bool useInternalHeader = true)
			: this(port, 1024, useInternalHeader)
		{

		}

		public TcpListener(int port, int bufferSize, bool useInternalHeader = true)
			: base(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp, useInternalHeader)
		{
			try
			{
				base.Port = port;

				base._pieceBufferSize = bufferSize;

				this._acceptedClients = new Dictionary<Guid, BaseSocket>();

				this._sendableClients = new List<Guid>();

				this._receivableOnlyClients = new List<Guid>();
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		#endregion

		#region 소켓 컨트롤

		/// <summary>
		/// 소켓을 수신 상태로 둔다.
		/// </summary>
		/// <returns>소켓이 수신 상태가 된 경우 true, 그렇지 않으면 false가 반환된다.</returns>
		public bool Listen()
		{
			try
			{
				IPAddress ipAddress = null;

				if (NetworkInformation.NetworkInformation.GetActiveIpAddress(out ipAddress))
				{
					base.Address = ipAddress;

					// 로컬 끝점을 정의한다.
					IPEndPoint localEndPoint = new IPEndPoint(base.Address, base.Port);

					base._socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true);

					// 소켓을 로컬 끝점과 연결한다.
					base._socket.Bind(localEndPoint);

					// 소켓을 수신 상태로 둔다.
					base._socket.Listen((int)SocketOptionName.MaxConnections);

					Synchronization.InvokeAndCallback(() => { this.IsListening = true; }, ListeningStarted, null);

					AcceptAsync();

					return true;
				}
			}
			catch (Exception error)
			{
				throw error;
			}

			return false;
		}

		/// <summary>
		/// 소켓의 수신 상태를 닫는다.
		/// </summary>
		public void Close()
		{
			try
			{
				if (!this.IsListening || base._socket == null) return;

				IList<BaseSocket> clients = this.Clients;

				foreach (BaseSocket client in clients)
				{
					client.DisconnectAsynchrous();
				}

				base._socket.Close();

				Synchronization.InvokeAndCallback(() => { this.IsListening = false; }, ListeningStopped, null);

				this.AcceptCompleted = null;
				this.ClientDisconnected = null;
				this.ListeningStarted = null;
				this.ListeningStopped = null;
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 연결 시도를 받아들이는 위한 비동기 작업을 실행한다.
		/// </summary>
		private void AcceptAsync()
		{
			base._socket.BeginAccept(BaseSocket._DEFAULT_BUFFER_SIZE, Synchronization.SynchronizeAsyncCallback((asyncResult) =>
			{
				Socket socket = null;

				try
				{
					int transferredSize = 0;
					byte[] buffer = new byte[transferredSize];

					// 클라이언트의 연결 시도를 받아들이고, 초기 데이터의 버퍼를 수신한다.
					socket = base._socket.EndAccept(out buffer, out transferredSize, asyncResult);

					if (buffer == null) return;

					Guid socketId;
					ClientType clientType = Sockets.ClientType.None;
					AcceptCondition acceptCondition;

					try
					{
						// 버퍼에서 연결 수용 정보를 분석한다.
						//if (this.UseInternalHeader)
						//{
						if (AcceptCondition.Parse(buffer, out acceptCondition, out clientType))
						{
							socketId = acceptCondition.Id;
						}
						else
						{
							socketId = Guid.NewGuid();
						}
						//}
						//else
						//{
						//	clientType = Sockets.ClientType.Unmanaged;

						//	socketId = Guid.NewGuid();
						//	transferType = Sockets.TransferType.Both;
						//}
					}
					catch (AcceptConditionParseException error)
					{
						// AcceptConditionParseException 예외가 발생한 경우 접속을 끊는다.
						socket.Disconnect(false);

						throw error;
					}

					BaseSocket client = new BaseSocket(socketId, socket, acceptCondition.Endian, clientType, this.UseInternalHeader, acceptCondition.UserToken);

					client.Disconnected = (e) =>
					{
						if (this._acceptedClients.ContainsKey(client.Id))
						{
							this._acceptedClients.Remove(client.Id);
						}

						Synchronization.Synchronize(ClientDisconnected, new AsyncResultEventArgs<BaseSocket>(client));
					};

					client.Error = (e) =>
					{
						base.ThrowError(e.Error);
					};

					client.Received = (e) =>
					{
						Synchronization.Synchronize(Received, new AsyncResultEventArgs<TransferEventArgs>(e.Result));
					};

					client.PartReceived = (e) =>
					{
						Synchronization.Synchronize(PartReceived, new AsyncResultEventArgs<long>(e.Result));
					};

					client.ReceiveAsynchrous();

					this._acceptedClients.Add(socketId, client);

					Synchronization.InvokeAndCallback(() => { return client; }, AcceptCompleted, null);
				}
				// 소켓이 닫힌 경우 예외가 발생한다.
				catch (ObjectDisposedException)
				{
					this._socket = null;

					return;
				}
				catch (AcceptConditionParseException error)
				{
					if (socket != null)
					{
						socket.Disconnect(false);
					}

					base.ThrowError(error);
				}
				catch (Exception error)
				{
					base.ThrowError(error);
				}
				finally
				{
					// 수신 상태일 경우에만 연결 시도를 받아들일 수 있다.
					if (this.IsListening)
					{
						this.AcceptAsync();
					}
				}
			}), null);
		}

		/// <summary>
		/// 모든 클라이언트로 데이터를 브로드캐스팅한다.
		/// </summary>
		/// <param name="data">브로드캐스팅할 데이터를 설정한다.</param>
		public void Broadcast(byte[] data)
		{
			try
			{
				IList<BaseSocket> clients = this.Clients;

				foreach (BaseSocket client in clients)
				{
					client.SendAsynchrous(data);
				}
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 모든 클라이언트로 객체를 브로드캐스팅한다.
		/// </summary>
		/// <param name="dataObject">브로드캐스팅할 객체를 설정한다.</param>
		public void Broadcast(object dataObject)
		{
			try
			{
				IList<BaseSocket> clients = this.Clients;

				foreach (BaseSocket client in clients)
				{
					client.SendObject(dataObject);
				}
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 모든 클라이언트로 메시지를 브로드캐스팅한다.
		/// </summary>
		/// <param name="dataMessage">브로드캐스팅할 메시지를 설정한다.</param>
		public void Broadcast(string dataMessage)
		{
			try
			{
				IList<BaseSocket> clients = this.Clients;

				foreach (BaseSocket client in clients)
				{
					client.SendMessage(dataMessage);
				}
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 특정 클라이언트로 데이터를 송신한다.
		/// </summary>
		/// <param name="id">클라이언트의 아이디 문자열을 설정한다.</param>
		/// <param name="data">송신할 데이터를 설정한다.</param>
		public void SendTo(string id, byte[] data)
		{
			try
			{
				BaseSocket client = this.FindClient(id);

				client.SendAsynchrous(data);
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 특정 클라이언트로 데이터 객체를 송신한다.
		/// </summary>
		/// <param name="id">클라이언트의 아이디 문자열을 설정한다.</param>
		/// <param name="dataObject">송신할 데이터 객체를 설정한다.</param>
		public void SendTo(string id, object dataObject)
		{
			try
			{
				BaseSocket client = this.FindClient(id);

				client.SendObject(dataObject);
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 특정 클라이언트로 데이터 메시지를 송신한다.
		/// </summary>
		/// <param name="id">클라이언트의 아이디 문자열을 설정한다.</param>
		/// <param name="dataMessage">송신할 데이터 메시지를 설정한다.</param>
		public void SendTo(string id, string dataMessage)
		{
			try
			{
				BaseSocket client = this.FindClient(id);

				client.SendMessage(dataMessage);
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		#endregion

		#region 클라이언트

		/// <summary>
		/// 클라이언트를 고유 코드로 찾는다.
		/// </summary>
		/// <param name="id">찾으려고 하는 클라이언트의 아이디 문자열을 설정한다.</param>
		/// <returns>아이디와 일치하는 클라이언트가 반환된다. 존재하지 않는 아이디인 경우에는 null이 반환된다.</returns>
		public BaseSocket FindClient(string id)
		{
			Guid guid;

			if (!Guid.TryParse(id, out guid)) return null;

			return this.FindClient(guid);
		}

		/// <summary>
		/// 클라이언트를 고유 코드로 찾는다.
		/// </summary>
		/// <param name="id">찾으려고 하는 클라이언트의 아이디를 설정한다.</param>
		/// <returns>아이디와 일치하는 클라이언트가 반환된다. 존재하지 않는 아이디인 경우에는 null이 반환된다.</returns>
		public BaseSocket FindClient(Guid id)
		{
			if (!this._acceptedClients.ContainsKey(id)) return null;

			return this._acceptedClients[id];
		}

		#endregion
	}
}